7章 高品質なルーチン
7.5までmoch5oMaki.icon
7章の導入部
高品質なルーチンについて述べるために品質の悪いルーチンを例にあげて考えていく
C++のサンプルコードそうですねという内容が並んでいる
レイアウトとか参照されていない引数などはわざわざ感もありましたが…moch5oMaki.icon
コメントがない、というのはどの程度のことだろう?moch5oMaki.icon
無駄にコメントを書かなくても中身が分かるのが良いコード、とも聞く
詳しくは32章を参照、とのことで私は上巻の19章までしか手元にないので一旦追求するのは諦めました
ルーチンはコンピュータサイエンスにおける最大の発明である
クラスも負けず劣らずの最大の発明であるらしい
ということでルーチンを使いこなしましょう、ということらしい
7章では「なぜルーチンを使いこなす必要があるのか」「どうやって使いこなすのか」という話題が展開されていく
7.1 ルーチンを作成する理由
なぜルーチンを使うのか?という話
筆者はルーチンを作成する主な理由がコードの重複を避けることだと考えていた
それだけではない、ここで詳細が説明されている
6章のクラスを作成する理由とも重なる
複雑さを低減する
処理の隠ぺい、コードサイズを最小限に抑える、ネストを独立させる
中間部分を分かりやすく抽象化する
処理を抽象化してルーチンの名前に集約する
コードの重複を避ける
これが最も一般的な理由
確かに、「ここでルーチンを作成すれば良い」とイメージしやすいし分かりやすいmoch5oMaki.icon
コードの量が減ると同時に変更のミスが少なくなるので保守性も向上する
サブクラスを作成しやすくする
オーバーライドが可能なルーチンを単純にしておくとサブクラスを実装するときにミスが少なくなる
処理順序を隠ぺいする
ポインタの処理を隠ぺいする
移植性を向上させる
移植性のない機能(言語の標準でない機能、ハードウェアやOSへの依存部分)を囲い込む
複雑な論理評価を単純にする
パフォーマンスを向上させる
全てのルーチンが小さくなくてはならないわけではない→7.4参照
7.2 ルーチンレベルでの設計
ルーチンの設計で気にするべきなのは凝集度であるという話
抽象化やカプセル化→クラスレベルで考察する
凝集度(ルーチン内の処理がどれだけ密に関連しているか)→ヒューリスティックな設計の原動力となる
ヒューリスティックというキーワードに否応なしに反応するようになってきたmoch5oMaki.icon
大事なんだろうなというのがひしひしと伝わる
凝集度に対する結合度の比率が最も高い(つまり凝集度が弱く密結合の)ルーチンは、その比率が最も低いルーチンの7倍もエラーが多く、修正に20倍ものコストがかかることがわかった(Selby and Basili 1991)
凝集度は意味的な話で、結合度は処理の依存関係の話
凝集度の種類
機能的凝集度=これを使いましょう!!
最も強い凝集度でルーチンが処理を一つだけ実行する場合。GetCustomerName()などの名前が示唆する通りの処理を行う
ーーここから先は凝集度が弱いので通常では理想的ではない、つまり出来る限り機能的凝集度を適用するようにする
情報的凝集度
決まった順序で実行される処理が集まったルーチン。各処理を切り出せば機能的に凝集させられる
連絡的凝集度
各処理が同じデータを使用するがそれ以外に関連性がないルーチン。上位と下位のルーチンに分けて別の処理をそれぞれルーチンに切り出すと機能的に凝集させられる
時間的凝集度
同時に実行される複数の処理をまとめたルーチン。時間的に凝集されたルーチンを他のイベントのまとめ役、と考えて扱う。つまり処理を細かいルーチンに切り出して、それを指揮する役割を上位のルーチンで担う形
ーーここから先はさらに凝集度が弱すぎて一般的には容認できないもの
手順的凝集度 ルーチンの処理が特定の順序で実行される場合
論理的凝集度 渡された制御フラ部によっていずれかの処理が選択される場合。ただし一連のif文やcase文と他のルーチンの呼び出しで構成される場合は良い設計である
暗号的凝集度 処理同士にそれと分かるような関わりがない場合
7.3 良いルーチン名
良いルーチン名の付け方についての話
ルーチンが行うことを全て説明する
ルーチンに副次効果があると、名前はどうも長くて稚拙なものになる。だからと言って、説明不足の名前をつけても解決にはならない。解決策は、副次効果のない直接的な処理を行うプログラムを作成することだ。
名前がつけにくい場合は設計を見直したほうがいい、みたいなことをよく聞くmoch5oMaki.icon
意味のない動詞、あいまいな動詞、どっちつかずの動詞を使わない
そもそも英語のニュアンスが掴めなくてどっちの方がいいんだろう?となることがしばしばmoch5oMaki.icon
HandleCaluculation(), PerformService() ~~といった名前では、そのルーチンが何をするのか伝わってこない。
名前の付け方が悪いだけなのか、それともルーチンがやっている処理自体があいまいのか??を考える必要がある
ルーチンは目的のあいまいさに苦しみ、その症状が意味の薄い名前となって現れる。
これも名前がつけにくい、というところに収束しそうmoch5oMaki.icon
ルーチン名を数字だけで区別しない
コードを15行分ずつ抜き出して、Part1, Part2といった名前の関数を作成した。それが済むと、それぞれの部分を呼び出す上位の関数を一つ作成した。
これは、、、流石に見たことないmoch5oMaki.icon
ある意味で楽、というのもある(コメントで補足するとか)
でも拡張性が、、、とはなる
必要な長さのルーチン名にする
ある調査によると、変数名の最適な長さは平均して9~15文字であるという。ルーチンは変数よりも複雑になりがちなので、良い名前をつけようと思うと、どうしても長くなりがちである。 〜中略〜 ルーチンの意味を理解するのに必要な長さにする。
関数名には戻り値の説明を反映させる
プロシージャ名には効果的な動詞とオブジェクトを使用する
言語の特性によって使い分ける
正確な反意語を使用する
これも英語ネイティブではないのがツライところ
やりたい処理に対して対になる処理の意味も考えてから付ければいい感じになりそうmoch5oMaki.icon
辞書的な意味だけではなく、プログラミングの文脈における意味での反意語とか考えるとちょっと違うパターンがある
pushとpopとか(本来の意味ではpushの反意語はpull)
一般的な処理の規約をまとめる
命名規則は、そのための最も簡単で最も信頼できる方法となる場合が多い
命名規則って最初にカチッと決めてる感じです??moch5oMaki.icon
最低限は決めといて細かいところはよしなにやっていく。プルリクとかで指摘しながらやっていく
インスタンス変数には決まったプレフィックスをつけるとか(でもJavaだったw)
ちゃんとしていると静的解析とかにも使えるのかもしれない
業務の中だと今関係しているところのコードから何となく空気を読んで、という感じになりがちな気がする
7.4 ルーチンの長さ
ルーチンの長さについてのもろもろの議論
理論的な最大の長さとしては、1画面分または1~2ページ文のプログラムリストに相当する50~150行が最適であると言われている。
ひとつのルーチンで150行って意外と長くていいんだなという感想moch5oMaki.icon
いろんな研究のエビデンスから=小さければ小さいほどいいということでもなく、100~150行くらいがベストっぽい
最大200行までは、ルーチンが長くなるとエラーが少なくなる
構造的な複雑さやデータの量がエラーに関係している
小さいルーチン(32行以下)とコストやエラーの発生率には相関関係はない
ルーチンが大きくなるほど(65行以上)、コード 1行あたりの開発コストは下がる
小さなルーチンの方が大きなルーチンに比べてエラー率は1.2倍だが、修正コストは2.4倍も少ない
コードの修正が最も発生しにくいのは100~150行のルーチンだった
最もエラーが発生しやすいのは500行以上のもので、500行を超えるとエラーの発生率はルーチンのサイズに比例する
でも結局長さは目安であって絶対ではない、という結論に達している
ルーチンの長さは、長さ自体に制限を設けることよりも、むしろルーチンの凝集度、ネストの深さ、変数の数、分岐の数、ルーチンを説明するのに必要なコメントの数、およびその他の複雑さに関する問題によって決定する。
ステータス管理するルーチンでifで10個くらい分岐している1500行のものに遭遇したakht.icon
最初は大したことない量だったんだろうけどだんだん複雑化した
最初のコードも大事だけど受託とかだとリファクタリングも厳しい。。。
7.5 ルーチンの引数の使用
この章では引数の利用のルールが述べられている
エラー全体の39%が内部のインターフェイスのエラー(ルーチン間のやりとりのエラー)であることが報告されている
引数は入力、変更、出力の順に配置する
引数は無作為またはアルファベット順に並べるのではなく、入力専用、入出力、出力専用の順に並べる。この順序はデータの入力、データの変更、結果の返送という、ルーチン内での処理の順序を反映している。
必ずしもこの順番でなくてもいいみたいだけど一貫性を持たせる必要がある、とのこと
引数の順序はあまり厳密に気にしたことなかった
次にAdaの引数リストの例を見てみよう。
Ada(エイダ)は、構造化・静的型付け・命令型・オブジェクト指向のパラダイムを持つ汎用プログラミング言語の一つである。構文はAlgol系である。史上初のプログラマとされるエイダ・ラブレスの名前にちなんでAdaと命名されている -Wikipediaより
複数のルーチンが似たような引数を使用する場合は、それらの順番を統一する
ルーチンの引数の順序は、記憶をよみがえらせるのに役立つ。順序が統一されていないと、引数を覚えるのが難しくなってしまう。
すべての引数を使用する
それはそう。ただし引数を使用しない理由がちゃんとあるならそれを残すのは構わない、とのこと。
状態変数またはエラー変数は最後に配置する
ルーチンの引数を作業用変数として使用しない
InputValを引数で渡して操作した後InputValをreturnしているルーチンの例
引数で渡した入力値(InputVal)はルーチン実行後に消えてしまっているので他の場所で思わぬエラーを生む可能性がある
引数に関するインターフェイスの条件を明記する
最近の言語であればアサーションを使えばコメントにしなくても十分よさそう
ルーチンの引数の数は大体7個に制限する
引数の数を増やすくせがついていることに気づいたら、それはルーチン間の結合度が強すぎる兆候である
同じデータを何種類ものルーチンに渡している場合は、それらのルーチンを1つのクラスにまとめ、頻繁に使用するデータをクラスデータとして扱うこと。
この目安はわかりやすいmoch5oMaki.icon
入力、変更、出力引数の命名規則を検討する
プレフィックスの活用
ルーチンのインターフェイスの抽象化を維持するために必要な変数またはオブジェクトを渡す
名前付きの変数を使用する あまり使うことなさそう
実引数係引数と一致することを確認する
7.6からakht.icon
7.6 関数の使用に関する注意点
関数
値を返すルーチン(純粋関数っぽいニュアンス)
値を返すが論理的にはプロシージャっぽいものもある
プロシージャ
値を返さないルーチン
7.6.1 関数とプロシージャの使い分け
要するに、ルーチンの主な目的が関数名が示す値を返すことであれば、関数を使用する。それ以外の場合は、プロシージャを使用する。
7.6.2 関数の戻り値の設定
(あんまり重要そうじゃなかったので割愛)
7.7 マクロルーチンとインラインルーチン
正しくマクロを書こう
マクロの式全体をかっこで囲む
複数行にわたるマクロを中かっこで囲む
このあたりに気をつけないと正しく展開されないよーという話
マクロを関数呼び出しの代わりに使用する手法は、一般に危険で理解しにくいため、悪いプログラミングプラクティスと見なされている。
7.7.1 マクロルーチンの使用の制限
C++ではマクロに代わる選択肢がいくつかある
定数値を宣言 const
インラインコードとしてコンパイルされる関数を定義 inline
min, maxなどの標準処理をタイプセーフに定義 template
列挙型を定義 enum
単純な型の置き換えを定義 typedef
「ほぼすべてのマクロは、プログラミング言語、プログラム、あるいはプログラマの不手際を浮き彫りにする。マクロを使用する際は、デバッガ、クロスリファレンス、プロファイラといったツールのサービスが満足に受けられないことを覚悟しておくべきだろう」(Stroustrup1997)
慎重なプログラマなら、通常はルーチンに代わる最後の手段としてマクロを使用する。
7.7.2 インラインルーチン
C++のinlineキーワードの話
ルーチン呼び出しのオーバヘッドのない、きわめて効率の良いコードの生成ができるらしい
インラインルーチンはカプセル化に違反する
C++ではインラインルーチンの実装コードをヘッダーファイルに含めないといけない
ヘッダーファイルを使うプログラマ全員に公開されることになる
インラインルーチンはあんまりピンとこない話だったakht.icon
ルーチンについてのチェックリスト
https://gyazo.com/3d886e166e55c82693ce342b3ec1269e
7.8 まとめ
ルーチンを作成する一番重要な理由は、プログラムを頭で理解しやすくすること
スペースを節約することはあまり重要じゃない
可読性、信頼性、保守性の方が重要
ルーチンとして独立させることが最も効果的なのは単純な処理のもの
ルーチンの凝集度は、機能的凝集度であることが望ましい
ルーチンの名前はその品質を表す
悪い名前はプログラムを変更する必要があることを意味する
関数を使うのは、関数の主目的が関数名で説明される特定の値を返すことである場合だけにすべき
sin(), CustomerID(), ScreenHeight()など
マクロは最後の手段